#include "app.hpp"
#include <cassert>
#include <iostream>
#include <iomanip>
#include <uuids.h>
#include <evcode.h>
#include "hrtext.hpp"
#include "mediatypeutil.hpp"

using std::cout;
using std::endl;
using GraphUtil::IPinPtr;
using GraphUtil::IBaseFilterPtr;
//using GraphUtil::IFileSourceFilterPtr;
using GraphUtil::FindOutpinVideo;
using GraphUtil::FindInpinVideo;

App::App(HANDLE hQuit) :
    m_hQuit(hQuit),
    m_input_filename(0),
    m_output_filename(0)
{
    assert(m_hQuit);
}


#if 1
int App::Parse(int argc, wchar_t* argv[])
{
    if (argc < 3)
    {
        cout << "too few args" << endl;
        return 1;
    }
    
    if (argc > 3)
    {
        cout << "too many args" << endl;
        return 1;
    }
    
    m_input_filename = argv[1];
    //test whether exists
    
    m_output_filename = argv[2];
    //if just directory, synthesize output name from input name
    //check available space on target HDD
    
    //TODO: implement the help feature used by xxcopy: if 
    //the /? switch doesn't stand alone, then print out 
    //all the variations of the other switches that were 
    //specified, and then exit.  Otherwise, if /? is alone,
    //print out the standard, generic list of switches.

    return 0;  //continue
}
#else
int App::Parse(int argc, wchar_t* argv[])
{
    //argc = 4
    //arg0  arg1  arg2  arg3  NULL
    //I                       J                       
    //                        K
    //The range [I, J) is what needs to be parsed.
    //The range [J, K) is what has already been parsed.
    //We want to rotate until we have processed all of the switches.
    //When we're finished, what is "unparsed" are all of the
    //the command-line args that aren't switches.
    //
    //We begin:
    //switch0  arg1  arg2 switch3 NULL
    //I                           J
    //                            K
    //We end:
    //arg1 arg2 switch0 switch3 NULL
    //I         J
    //                          K
    //But really, maybe what we have is:
    //arg1 arg2 switch0 switch3 NULL
    //          I
    //          J
    //                          K
    //We need a termination condition, and I think I=J is it.
    //
    //If I designates an arg, then
    //1:
    //arg2 switch0 switch3 arg1 NULL
    //I                    J
    //                          K
    //
    //2:
    //switch0 switch3 arg1 arg2 NULL
    //I               J
    //                          K
    //
    //3:
    //switch0 switch3 arg1 arg2 NULL
    //        I                   
    //                J
    //                          K
    //
    //3:
    //switch0 switch3 arg1 arg2 NULL
    //                I                   
    //                J
    //                          K
    //
    //If we detect a switch, we permute.
    //Otherwise, we increment.
    //
    //But really, we permute: we change the content of 
    //the array, not moving the iterator.
    //
    //arg2 switch0 switch3 arg1 NULL
    //I                    
    //                     J
    //                          K
    //
    //switch0  arg1  arg2 switch3 NULL
    //arg1  arg2  switch3 NULL switch0
    //arg1  arg2  NULL switch0 switch3
    //
    //switch0  arg1  arg2 switch3 NULL
    //I
    //                            J
    //                            K
    //arg1  arg2  switch3  switch0  NULL
    //I         
    //                              J
    //                              K
    //
    //
    
    typedef const wchar_t** iter_t;
    
    const iter_t first = iter_t(argv);
    const iter_t last = first + argc;
    
    iter_t i = first;
    iter_t j = last;
    
    //[first, i) are args (non-switches)
    //[i, j) are unparsed
    //[j, last) are switches
    
    while (i != j)
    {
        const wchar_t* s = *i;
        
        if (*s == L'/')
        {
            if (int status = ProcessSwitchShort(i, j))
                return status;
        }
        else if (*s != L'-')
        {
            ++i;
            continue;
        }            

        if (!++s)
        {
            os << "bad switch syntax: hyphen cannot stand alone" << endl;
            return 1;  //error
        }
        
        if (*s == L'-')
        {
            if (int status = ProcessSwitchLong(i, j))
                return status;
        }
        else
        {
            if (int status = ProcessSwitchShort(i, j))
                return status;
        }
    }
    
    return 0;
}
#endif


int App::operator()(int argc, wchar_t* argv[])
{
    if (int status = Parse(argc, argv))
        return status;
        
    if (int status = CreateGraph())
        return status;
        
    const int status = RunGraph();
    
    DestroyGraph();
    
    return status;
}


#if 0
bool App::IsSwitch(const wchar_t* str)
{
    assert(str);
    
    const wchar_t c = *str;
    
    if (c == L'-')
        return true;
        
    if (c == L'/')
        return true;
        
    return false;
}


int App::ProcessSwitch(
    const wchar_t**& i,
    const wchar_t* const* j)
{
    assert(i);
    assert(j);
    assert(i != j);
    assert(*i);
    
    const wchar_t* s = *i;
    assert(IsSwitch(s));

    if (*s == L'/')
        return ProcessSwitchShort(i, j);

    assert(*s == 
}
#endif


int App::CreateGraph()
{
    assert(!bool(m_pGraph));
    assert(!bool(m_pSeek));
    
    HRESULT hr = m_pGraph.CreateInstance(CLSID_FilterGraphNoThread);
    assert(SUCCEEDED(hr));
    assert(bool(m_pGraph));
    
    const GraphUtil::IMediaFilterPtr pGraphFilter(m_pGraph);
    assert(bool(pGraphFilter));
    
    //IMediaFilter::SetSyncSource
    //ms-help://MS.VSCC.2003/MS.MSDNQTR.2003FEB.1033/directshow_sp1/htm/imediafiltersetsyncsource.htm
    //
    //Reference Clocks
    //ms-help://MS.VSCC.2003/MS.MSDNQTR.2003FEB.1033/directshow_sp1/htm/referenceclocks.htm
    
    hr = pGraphFilter->SetSyncSource(0);  //process as quickly as possible
    //TODO: are we setting this too early?
    
    //see vtsreadfilter.cpp
    const wchar_t strVTSRead[] = L"{AEEA8CAC-9FDA-49ca-8001-1B984E8F1663}";
    const IBaseFilterPtr pReader(strVTSRead);
    assert(bool(pReader));
    assert(GraphUtil::PinCount(pReader) == 1);
    
    hr = m_pGraph->AddFilter(pReader, L"vtsread");
    assert(SUCCEEDED(hr));
    
    const GraphUtil::IFileSourceFilterPtr pFileSource(pReader);
    assert(bool(pFileSource));
    
    hr = pFileSource->Load(m_input_filename, 0);
    
    if (FAILED(hr))
    {
        cout << "Unable to load input file \""
             << m_input_filename
             << "\"."
             << endl;
             
        return 1;
    }
    
    //CLSID_MMSPLITTER    
    //TODO: provide option to choose which splitter: "MMSPLITTER" or "MPEG2Demultiplexer"
    //TODO: add support for manipulating the IAMStreamSelect interface (applies
    //to MMSPLITTER only?).
#if 1
    const IBaseFilterPtr pDemux(CLSID_MMSPLITTER);
    assert(bool(pDemux));
    assert(GraphUtil::InpinCount(pDemux) == 1);
#else
    //I cannot successfully connect the async file reader
    //to the mpeg2demultiplexer.  The splitter works fine.
    //The demultiplexer always returns this error:
    //
    // MessageId: VFW_E_UNKNOWN_FILE_TYPE
    //
    // MessageText:
    //
    // The media type of this file is not recognized.%0
    //
    //#define VFW_E_UNKNOWN_FILE_TYPE          ((HRESULT)0x80040240L)
    //
    //After many attemps it does connect, but then it immediately
    //disconnects.  I didn't try explicitly configuring the demultiplexer
    //(which is req'd, although I thought only when in push mode, which
    //this is not), so it's either that, or it doesn't like the 
    //media type.

    const IBaseFilterPtr pDemux(CLSID_MPEG2Demultiplexer);
    assert(bool(pDemux));
    assert(GraphUtil::InpinCount(pDemux) == 1);
    
    //Maybe the solution is to use ConnectDirect with with 
    //a non-NULL media type.
#endif
    
    hr = m_pGraph->AddFilter(pDemux, L"splitter");
    assert(SUCCEEDED(hr));
    
    hr = GraphUtil::ConnectDirect(m_pGraph, pReader, pDemux);
    
    if (FAILED(hr))
    {
        cout << "Unable to connect reader to splitter." << endl;
        return 1;
    }
    
    const IPinPtr pDemuxOutpinVideo(FindOutpinVideo(pDemux));
    
    if (!bool(pDemuxOutpinVideo))
    {
        cout << "Splitter has no video outpin." << endl;
        return 1;
    }
    
    //see mkvmuxfilter.cpp
    const wchar_t strMkvMux[] = L"{E1642F6D-181E-41ab-9FFB-9321D3A5DD09}";
    const IBaseFilterPtr pMux(strMkvMux);
    assert(bool(pMux));
    assert(GraphUtil::OutpinCount(pMux) == 1);
    
    hr = m_pGraph->AddFilter(pMux, L"mkvmux");
    assert(SUCCEEDED(hr));
    
    const IPinPtr pMuxInpinVideo(FindInpinVideo(pMux));
    assert(bool(pMuxInpinVideo));
    
    hr = m_pGraph->ConnectDirect(pDemuxOutpinVideo, pMuxInpinVideo, 0);
    
    if (FAILED(hr))
    {
        cout << "Unable to connect splitter video outpin to muxer video inpin." << endl;
        return 1;
    }
    
#if 0
    _COM_SMARTPTR_TYPEDEF(IAMStreamSelect, __uuidof(IAMStreamSelect));
    
    const IAMStreamSelectPtr pSelect(pDemux);
    assert(bool(pSelect));
        
    DWORD count;
    
    hr = pSelect->Count(&count);
    
    if (SUCCEEDED(hr))
    {
        for (DWORD i = 0; i < count; ++i)
        {        
            AM_MEDIA_TYPE* pmt;
            DWORD dwFlags;
            LCID lcid;
            DWORD dwGroup;
            wchar_t* strName;
            IUnknown* pObj;
            IUnknown* pUnk;
            
            hr = pSelect->Info(i, &pmt, &dwFlags, &lcid, &dwGroup, &strName, &pObj, &pUnk);
            
            if (pObj)
                pObj->Release();
                
            if (pUnk)
                pUnk->Release();
                
            MediaTypeUtil::Free(pmt);
            CoTaskMemFree(strName);            
        }
    }
#endif

    const IBaseFilterPtr pWriter(CLSID_FileWriter);
    assert(bool(pWriter));
    assert(GraphUtil::InpinCount(pWriter) == 1);
    
    m_pGraph->AddFilter(pWriter, L"writer");
    assert(SUCCEEDED(hr));
    
    const GraphUtil::IFileSinkFilterPtr pSink(pWriter);
    assert(bool(pSink));
    
    hr = pSink->SetFileName(m_output_filename, 0);
    
    if (FAILED(hr))
    {
        cout << "Unable to set output filename of file writer filter." << endl;
        return 1;
    }
    
    hr = GraphUtil::ConnectDirect(m_pGraph, pMux, pWriter, 0);
    
    if (FAILED(hr))
    {
        cout << "Unable to connect muxer to writer." << endl;
        return 1;
    }
    
    m_pSeek = pMux;
    assert(bool(m_pSeek));
    
    return 0;  //success   
}



void App::DestroyGraph()
{
    m_pSeek = 0;
    
    if (IFilterGraph* pGraph = m_pGraph.Detach())
    {
        const ULONG n = pGraph->Release();
        assert(n == 0); n;
    }
}


int App::RunGraph()
{
    assert(bool(m_pGraph));
    
    const GraphUtil::IMediaEventPtr pEvent(m_pGraph);
    assert(bool(pEvent));

    HANDLE h;
    
    HRESULT hr = pEvent->GetEventHandle((OAEVENT*)&h);
    assert(hr == S_OK);
    assert(h);
    
    //DWORD dw = WaitForSingleObject(h, 0);
    
    enum { nh = 2 };
    const HANDLE ha[nh] = { m_hQuit, h };
    
    const GraphUtil::IMediaControlPtr pControl(m_pGraph);
    assert(bool(pControl));
    
    hr = pControl->Run();
    assert(SUCCEEDED(hr));
    
    //int n = 1;
    
    for (;;)
    {
        MSG msg;
        
        while (PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
        {
            assert(msg.message != WM_QUIT);
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        
        const DWORD dw = MsgWaitForMultipleObjects(
                            nh,
                            ha,
                            0,
                            100,  //timeout (ms)
                            QS_ALLINPUT);
                    
        if (dw == WAIT_TIMEOUT)
        {
#if 0
            cout << '\r';  //carriage return
                
            int i = 0;
            
            for (; i < n; ++i)
                cout << '.';
                
            for (; i < 10; ++i)
                cout << ' ';
                
            if (++n >= 10)
                n = 1;
#else
            //TODO: display this (or give option to) in HH:MM:SS.sss
            //TODO: attempt to query upstream filter for duration.
            
            if (bool(m_pSeek))
            {
                __int64 curr;

                hr = m_pSeek->GetCurrentPosition(&curr);
                assert(SUCCEEDED(hr));
                assert(curr >= 0);
                
                const double t = double(curr) / 10000000;
                
                cout << "\rtime[sec]=" 
                     << std::fixed 
                     //<< std::setfill(' ') 
                     << std::setprecision(1)
                     << t
                     << std::flush;
            }
#endif
            
            continue;
        }

        assert(dw >= WAIT_OBJECT_0);
        assert(dw <= (WAIT_OBJECT_0 + nh));
        
        if (dw == WAIT_OBJECT_0)  //quit
            break;
            
        if (dw == (WAIT_OBJECT_0 + nh))  //window message
            continue;
            
        //media event
        
        long code, param1, param2;
        
        HRESULT hr = pEvent->GetEvent(&code, &param1, &param2, 0);
        assert(hr == S_OK);
        
        hr = pEvent->FreeEventParams(code, param1, param2);
        assert(hr == S_OK);
        
        if (code == EC_USERABORT) //window closed
            break;
            
        if (code == EC_COMPLETE)
            break;
    }
    
    cout << endl;
    
    hr = pControl->Stop();
    assert(SUCCEEDED(hr));
    
    return 0;
}
